﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using System.Reflection;

using Antix;
using Antix.Data;
using Antix.Data.Objects;
using Antix.Data.Catalogue.Objects;

namespace Antix.ObjectsToDatabase
{
    public class Builder
    {
        #region Generate

        /// <summary>
        /// <para>Initialise the application</para>
        /// </summary>
        public string Generate(Assembly assembly, Database db)
        {
            // check database structure against project types
            try
            {
                // Add tables for all objects
                Generate_AddTables(assembly, db);
                foreach (Assembly refAssembly in AppDomain.CurrentDomain.GetAssemblies())
                {
                    Generate_AddTables(refAssembly, db);
                }

                // Add references for all objects
                Generate_AddReferences(assembly, db);
                foreach (Assembly refAssembly in AppDomain.CurrentDomain.GetAssemblies())
                {
                    Generate_AddReferences(refAssembly, db);
                }

                return db.Script();
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        #region Helper Functions

        /// <summary>
        /// <para>Add referenced objects</para>
        /// </summary>
        /// <param name="assembly"></param>
        /// <param name="db"></param>
        private void Generate_AddReferences(
            Assembly assembly, Database db)
        {
            try
            {
                foreach (Type type in assembly.GetTypes())
                {
                    // check type implements IObject
                    if (!type.IsAbstract)
                    {
                        DataTableAttribute tableAtt = type.GetDataTable();
                        if (tableAtt != null)
                        {
                            DatabaseTable tbl = this.FindTable(tableAtt, db.Tables);

                            // Check the fields
                            foreach (DataColumnAttribute colAtt in type.GetDataColumns())
                            {
                                if (colAtt != null)
                                {
                                    Type refType = colAtt.ReferencedType != null
                                        ? colAtt.ReferencedType
                                        : colAtt.FieldInfo.FieldType;
                                    if (colAtt.Create)
                                    {
                                        DataTableAttribute refTableAtt = refType.GetDataTable();
                                        if (refTableAtt != null)
                                        {
                                            // set the column name if not specified
                                            if (colAtt.Name == null) { colAtt.Name = colAtt.FieldInfo.Name.TrimStart('_'); }
                                            DatabaseColumn col = tbl.Columns[colAtt.Name];
                                            DatabaseTable refTbl = this.FindTable(refTableAtt, db.Tables);

                                            string refName = string.Format("{0}.{1}.{2}_{3}.{4}",
                                                tbl.Schema.Name, tbl.Name, col.Name,
                                                refTbl.Schema.Name, refTbl.Name);
                                            DatabaseReference dr = db.References[refName, true];
                                            dr.Column = col;
                                            dr.ReferencedTable = refTbl;
                                            dr.IsDeleted = false;

                                            dr.DeleteAction = colAtt.DeleteAction;
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
            catch (Exception) { }
        }

        /// <summary>
        /// Add types as tables from the give assembly
        /// </summary>
        /// <param name="assembly">Assembly</param>
        /// <param name="db">database</param>
        private void Generate_AddTables(Assembly assembly, Database db)
        {
            try
            {
                foreach (Type type in assembly.GetTypes())
                {
                    // check type
                    if (!type.IsAbstract)
                    {
                        DataTableAttribute tableAtt = type.GetDataTable();
                        if (tableAtt != null)
                        {
                            // find the table
                            DatabaseTable tbl = this.FindTable(tableAtt, db.Tables);

                            if (tbl != null)
                            {
                                if (tbl.Name != tableAtt.Name
                                    || tbl.Schema.Name != tableAtt.Schema)
                                {
                                    tbl.Name = tableAtt.Name;
                                    tbl.Schema = db.Schemas[tableAtt.Schema, true];
                                }
                            }
                            else
                            {
                                // create new one if not found
                                tbl = db.Tables.Add(tableAtt.Schema, tableAtt.Name);
                            }

                            #region Check the fields

                            foreach (DataColumnAttribute colAtt in type.GetDataColumns())
                            {
                                Type fieldType = colAtt.FieldInfo.FieldType.GetNotNullableType();

                                if (
                                    colAtt != null && colAtt.Create
                                    && fieldType.GetInterface("ICollection") == null)
                                {
                                    // set the column name if not specified
                                    if (colAtt.Name == null) { colAtt.Name = colAtt.FieldInfo.Name.TrimStart('_'); }

                                    // add the column and set its properties
                                    DatabaseColumn col = FindColumn(colAtt, tbl.Columns);

                                    // create new one if not found
                                    if (col == null) { col = tbl.Columns.Add(colAtt.Name); }
                                    else
                                    {
                                        // set the name incase its changed
                                        col.Name = colAtt.Name;
                                    }

                                    // Check for allow null
                                    col.AllowNull = colAtt.AllowNull || fieldType.IsNullable();

                                    // set the column according to the field type
                                    if (fieldType.GetDataTable() != null)
                                    {
                                        db.SetTypeToColumn(
                                            typeof(int),
                                            0, false,
                                            colAtt.Default,
                                            col);
                                    }
                                    else
                                    {
                                        db.SetTypeToColumn(
                                            fieldType,
                                            colAtt.Length, colAtt.VariableLength,
                                            colAtt.Default,
                                            col);
                                    }
                                }
                            }

                            #endregion

                            #region Check the indexes

                            foreach (DataIndexAttribute indexAtt in type.GetDataIndex())
                            {
                                DatabaseIndex idx = FindIndex(indexAtt, tbl.Indexes);

                                // create new one if not found
                                if (idx == null) { idx = tbl.Indexes.Add(indexAtt.Name); }
                                else
                                {
                                    // set the name incase its changed
                                    idx.Name = indexAtt.Name;
                                }

                                idx.IsUnique = indexAtt.IsUnique;
                                idx.Columns = indexAtt.Columns;
                            }

                            #endregion
                        }
                    }
                }
            }
            catch (Exception) { }
        }

        /// <summary>
        /// Get the path given an assembly
        /// </summary>
        public static string GetAssemblyPath(Assembly assembly)
        {
            string path = new Uri(assembly.CodeBase).AbsolutePath;
            return path.Substring(0, path.LastIndexOf("/"));
        }

        /// <summary>
        /// <para>Get the table using the table attribute</para>
        /// <para>This will search for renamed objects too</para>
        /// </summary>
        private DatabaseTable FindTable(DataTableAttribute tableAtt, DatabaseTableCollection tables)
        {
            DatabaseTable tbl = tables[tableAtt.Schema, tableAtt.Name];
            if (tbl == null
                && tableAtt.NameOriginal != null) { tbl = tables[tableAtt.Schema, tableAtt.NameOriginal]; }
            if (tbl == null
                && tableAtt.SchemaOriginal != null) { tbl = tables[tableAtt.SchemaOriginal, tableAtt.Name]; }
            if (tbl == null
                && tableAtt.NameOriginal != null
                && tableAtt.SchemaOriginal != null) { tbl = tables[tableAtt.SchemaOriginal, tableAtt.NameOriginal]; }

            return tbl;
        }

        /// <summary>
        /// <para>Get the column using the column attribute</para>
        /// <para>This will search for renamed objects too</para>
        /// </summary>
        private DatabaseColumn FindColumn(DataColumnAttribute colAtt, DatabaseColumnCollection columns)
        {
            DatabaseColumn col = columns[colAtt.Name];
            if (col == null
                && colAtt.NameOriginal != null) { col = columns[colAtt.NameOriginal]; }

            return col;
        }


        /// <summary>
        /// <para>Get the Index using the Index attribute</para>
        /// <para>This will search for renamed objects too</para>
        /// </summary>
        private DatabaseIndex FindIndex(DataIndexAttribute idxAtt, DatabaseIndexCollection indexes)
        {
            DatabaseIndex idx = indexes[idxAtt.Name];
            if (idx == null
                && idxAtt.NameOriginal != null) { idx = indexes[idxAtt.NameOriginal]; }

            return idx;
        }

        #endregion

        #endregion

    }
}